package com.jungle.base.dao;

import com.jungle.base.criteria.Condition;
import com.jungle.base.utils.ReflectUtil;
import io.micrometer.common.util.StringUtils;
import jakarta.persistence.criteria.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.jpa.domain.Specification;

import java.lang.reflect.Field;
import java.util.*;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.function.UnaryOperator;

public class SpecBuilder<T> {
    private static final Logger log = LoggerFactory.getLogger(SpecBuilder.class);
    private Specification<T> specification;


    /**
     * Converts the given conditions object into a Spring Data JPA Specification object.
     * This method provides a generic way to transform application-defined query conditions into the Specification format used by Spring Data JPA.
     * It abstracts the conversion details, allowing developers to focus more on business logic rather than the conversion process.
     *
     * @param queryConditions An object containing the query conditions. The specific type and structure of this object depend on the application's requirements.
     */
    public SpecBuilder(final Object queryConditions) {
        // Uses the SpecificationHelper class to handle the conversion from the conditions object to the Specification object.
        // SpecificationHelper is a utility class that encapsulates the conversion logic, simplifying the external invocation.
        this(queryConditions, true);
    }

    public SpecBuilder() {
        this(null, true);
    }

    /**
     * Converts the given query conditions into a Specification.
     * This method is used to dynamically generate database query specifications based on the provided conditions.
     *
     * @param and A boolean value that determines whether the logical relationship between query conditions is AND or OR.
     */
    public SpecBuilder(final Object queryConditions, boolean and) {
        this.specification = (root, query, cb) -> (queryConditions == null) ? null : toPredicate(root, query, cb, queryConditions, and);
    }

    public Specification<T> toSpec() {
        // Return the final specification
        // 同级中，清除重复比较条件
        return specification;
    }

    public SpecBuilder<T> and(Specification<T> spec) {
        this.specification = specification.and(spec);
        return this;
    }

    public SpecBuilder<T> or(Specification<T> spec) {
        this.specification = specification.or(spec);
        return this;
    }

    public SpecBuilder<T> extend(UnaryOperator<Specification<T>> spec) {
        this.specification = spec.apply(this.specification);
        return this;
    }

    /**
     * Converts query conditions to a JPQL Predicate object.
     *
     * @param queryConditions The query conditions object. Fields of this object are used as query conditions via reflection.
     * @param root            The Root object representing the entity being queried.
     * @param cb              The CriteriaBuilder object used to construct the Predicate.
     * @param and             A boolean flag indicating whether to create a conjunction (AND) or disjunction (OR).
     * @return A Predicate object representing the query conditions.
     */
    private static <T> Predicate toPredicate(Root<T> root, CriteriaQuery<?> query, CriteriaBuilder cb, Object queryConditions, boolean and) {

        // Retrieve all fields from the query conditions class
        List<Field> fieldList = ReflectUtil.findAllField(queryConditions.getClass(), new ArrayList<>());

        // Obtain a function for generating Path objects based on the Root
        BiFunction<Condition, String, Path<?>> function = pathFunction(root);

        // Stream over the fields, map them to predicates, filter out nulls, and add them to the predicate
        List<Predicate> predicates = fieldList.stream()
                .map(toExpression(cb, queryConditions, function))
                .filter(Objects::nonNull).toList();
        Predicate[] aryPredicate = predicates.toArray(new Predicate[0]);
        return and ? cb.and(aryPredicate) : cb.or(aryPredicate);
    }


    /**
     * Converts a field to a conditional expression based on query conditions.
     * This method is used to construct query conditions dynamically based on annotations and field values.
     *
     * @param cb              The CriteriaBuilder object, used to create conditional expressions.
     * @param queryConditions The object containing the query conditions, used to retrieve field values.
     * @return Returns a Function that accepts a Field object and returns a conditional expression.
     */
    private static Function<Field, Predicate> toExpression(CriteriaBuilder cb, Object queryConditions, BiFunction<Condition, String, Path<?>> function) {
        return field -> {
            // Check if the field is annotated with Condition, which indicates it is a query condition field.
            if (field.isAnnotationPresent(Condition.class)) {
                Condition annotation = field.getAnnotation(Condition.class);
                // Determine the column name to use, defaulting to the field name if not specified in the annotation.
                String column = annotation.column().isEmpty() ? field.getName() : annotation.column();
                // Retrieve the value of the field in the queryConditions object.
                Object value = ReflectUtil.getFieldValue(queryConditions, field);
                // Construct the access path for the entity field based on the query condition.
                Path<?> path = function.apply(annotation, column);

                // Construct the conditional expression based on the query condition, path, and value.
                return buildExpression(cb, path, annotation, value);
            } else {
                // Return null if the field is not a query condition field.
                return null;
            }
        };
    }

    private static <T> BiFunction<Condition, String, Path<?>> pathFunction(Root<T> root) {
        return (annotation, attributeName) -> {
            String joinFiled = annotation.joinField();
            return StringUtils.isBlank(joinFiled) ? root.get(attributeName) : root.join(joinFiled, toJoinType( annotation.joinType())).get(attributeName);
        };
    }

    private static JoinType toJoinType(com.jungle.base.criteria.JoinType joinType) {
        return switch (joinType) {
            case LEFT -> JoinType.LEFT;
            case RIGHT -> JoinType.RIGHT;
            default -> JoinType.INNER;
        };
    }

    private static Predicate equalExpression(CriteriaBuilder cb, Path<?> path, Object value) {
        return cb.equal(path, value);
    }

    private static Predicate numberExpression(CriteriaBuilder cb, Condition condition, Path<Number> path, Number value) {
        return switch (condition.func()) {
            case equal -> cb.equal(path, value);
            case gt -> cb.gt(path, value);
            case ge -> cb.ge(path, value);
            case lt -> cb.lt(path, value);
            case le -> cb.le(path, value);
            case notEqual -> cb.notEqual(path, value);

            default -> null;
        };
    }

    private static Predicate stringExpression(CriteriaBuilder cb, Condition condition, Path<String> path, String str) {
        return switch (condition.func()) {
            case equal -> cb.equal(path, str);
            case like -> cb.like(path, "%".concat(str).concat("%"));
            case startsWith -> cb.like(path, str.concat("%"));
            case endsWith -> cb.like(path, "%".concat(str));
            case notLike -> cb.notLike(path, "%".concat(str).concat("%"));
            default -> null;
        };
    }

    /**
     * Builds an expression based on the given condition and value.
     * This method constructs a boolean expression according to the provided condition object, path object, and value object.
     * It is primarily used for dynamically generating query conditions, supporting various types of values.
     *
     * @param cb        the CriteriaBuilder used to construct the expression
     * @param condition the condition object that defines the type of comparison (e.g., equal, greater than)
     * @param path      the path object representing the field in the query
     * @param value     the value to be compared against the field
     * @return an Expression<Boolean> representing the constructed boolean expression
     */
    private static Predicate buildExpression(CriteriaBuilder cb, Path<?> path, Condition condition, Object value) {
        // Handle null values based on whether they are allowed.
        if (value == null) {
            return condition.nullable() ? cb.isNull(path) : null;
        }
        // Handle non-primitive types
        else if (value instanceof Number number) {
            // comparisons for numeric operands:
            return numberExpression(cb, condition, (Path<Number>) path, number);
        }
        // Handle string types
        else if (value instanceof String str) {
            // Special handling for string types, considering empty strings and whether blanks are allowed.
            if (str.isEmpty() && !condition.blank()) {
                return null;
            }
            return stringExpression(cb, condition, (Path<String>) path, str);
        }
        // Handle date types
        else if (value instanceof Date date) {
            return dateExpression(cb, path, date);
        }
        // Handle boolean and character types
        else if (value instanceof Boolean || value instanceof Character) {
            return equalExpression(cb, path, value);
        }


        Class<?> valueClass = value.getClass();
        // Handle collections
        if (Collection.class.isAssignableFrom(valueClass)) {
            return toPredicate(cb, path, (Collection<?>) value);
        }
        // Handle arrays
        else if (valueClass.isArray()) {
            return toPredicate(cb, path, (Object[]) value);
        }
        // Handle primitive types
        else if (valueClass.isPrimitive()) {
            String typeName = valueClass.getSimpleName();
            return switch (typeName) {
                case "int", "long", "short", "float", "double" ->
                        numberExpression(cb, condition, (Path<Number>) path, (Number) value);
                case "byte", "char", "boolean" -> equalExpression(cb, path, value);
                default -> throw new IllegalStateException("Unexpected value: " + typeName);
            };
        }

        return null;
    }

    private static Predicate dateExpression(CriteriaBuilder cb, Path path, Date value) {
        return cb.equal(path, value);
    }


    private static Predicate doCollectionPredicate(Object value, Function<Collection<?>, Predicate> function) {
        if (value instanceof Collection) {
            return function.apply((Collection) value);
        } else if (value instanceof Object[]) {
            return function.apply(Arrays.asList((Object[]) value));
        } else {
            log.error("The value is not a collection or array.");
            return null;
        }
    }

    private static Function<Collection<?>, Predicate> doIn(CriteriaBuilder cb, Path<?> path) {
        return collection -> {
            collection.removeIf(Objects::isNull);
            if (collection.size() == 1) {
                return cb.equal(path, collection.iterator().next());
            } else if (collection.size() > 1) {
                return path.in(collection);
            } else {
                log.error("The collection is empty.");
                return null;
            }
        };
    }

    private static Function<Collection<?>, Predicate> doNotIn(CriteriaBuilder cb, Path<?> path) {
        return collection -> {
            collection.removeIf(Objects::isNull);
            if (collection.size() == 1) {
                return cb.notEqual(path, collection.iterator().next());
            } else if (collection.size() > 1) {
                return cb.not(path.in(collection));
            } else {
                log.error("The collection is empty.");
                return null;
            }
        };
    }


    /**
     * Constructs a predicate to check if the value of the given path is within a specified range.
     * This method is primarily used to handle "in" type query conditions, i.e., checking if a field's value is within a given collection or array.
     *
     * @param cb    The CriteriaBuilder object used to construct query conditions.
     * @param path  The Path object representing the field path in the query condition.
     * @param value An object that can be a Collection or an array, containing the range of values to compare.
     * @return A Predicate object representing the constructed query condition; returns null if the value type does not match.
     */
    private static Predicate between(CriteriaBuilder cb, Path<?> path, Object value) {
        // If value is a Collection, call a specific method to build the predicate
        if (value instanceof Collection) {
            return toPredicate(cb, path, (Collection) value);
        } else if (value.getClass().isArray()) {
            // If value is an array, call a specific method to build the predicate
            return toPredicate(cb, path, (Object[]) value);
        } else {
            // If value is neither a Collection nor an array, log a warning and return null
            log.warn("between,value:{}", value.getClass().getName());
            return null;
        }
    }


    private static Predicate toPredicate(CriteriaBuilder builder, Path path, Collection<?> collection) {
        Iterator<?> iterator = collection.iterator();
        Object a = iterator.hasNext() ? iterator.next() : null;
        Object b = iterator.hasNext() ? iterator.next() : null;
        return toPredicate(builder, path, a, b);
    }

    private static Predicate toPredicate(CriteriaBuilder builder, Path path, Object[] ary) {
        Object a = ary.length > 0 ? ary[0] : null;
        Object b = ary.length > 1 ? ary[1] : null;
        return toPredicate(builder, path, a, b);
    }

    private static Predicate toPredicate(CriteriaBuilder builder, Path path, Object a, Object b) {
        if (a == null && b != null) {
            return builder.lessThanOrEqualTo(path, (Comparable) b);
        } else if (a != null && b == null) {
            return builder.greaterThanOrEqualTo(path, (Comparable) a);
        } else if (a != null && a instanceof Comparable) {
            Comparable ca = (Comparable) a;
            Comparable cb = (Comparable) b;
            return ca.compareTo(cb) == -1 ? builder.between(path, ca, cb) : builder.between(path, cb, ca);
        } else {
            return null;
        }
    }
}
